누적 카운터 집계 함정
NOTE
장비/센서 등이 “누적값”을 계속 보내오는 데이터에서 기간별 증분을 집계할 때 흔히 걸리는 세 가지 함정 — 잘못된 소스 테이블 선택, 합산 후 차감의 위험,
MAX(CASE...)오용 — 과 해결 패턴. 실무(장비 추출 횟수 집계)에서 추출·일반화.
배경 — “누적값”을 다루는 통계의 공통 구조
일부 장비/카테고리는 절대 카운터를 계속 누적해서 보고한다(리셋 전까지 계속 증가). 특정 기간의 “그 기간 동안 발생한 증분”을 구하려면 보통 (기간 종료 시점 누적값) - (기간 시작 시점 누적값)을 계산한다. 이 단순한 계산이 아래 세 가지 이유로 자주 어긋난다.
함정 1 — 이미 합산된 테이블을 쓰면 세부 축이 사라진다
원본 데이터가 “카테고리 A + 카테고리 B + … 단위로 이미 합산된” 요약 테이블과, “세부 카테고리별로 나뉜” 원천 테이블 두 가지로 존재할 수 있다. 세부 카테고리가 중간에 바뀐 이력이 있는 경우, 이미 합산된 요약 테이블만으로는 정확한 증분을 복원할 수 없다 — 합산 시점에 세부 축 정보가 이미 사라졌기 때문이다.
해결: 세부 카테고리 축이 유지되는 원천(raw) 누적 테이블을 기준으로 집계한다. 합산 테이블은 “이미 계산된 결과를 빠르게 보여주는 캐시” 정도로만 쓰고, 정확한 재계산이 필요하면 원천 테이블로 내려간다.
함정 2 — “합산 후 차감”은 한 축의 이상치가 다른 축을 오염시킨다
여러 세부 카테고리(축)의 누적값을 각각 “최종 - 최초”로 구하지 않고, 전체를 먼저 더한 뒤 한 번에 빼면 문제가 생긴다.
잘못된 방식: SUM(최종 누적 전체) - SUM(최초 누적 전체)이 방식은 특정 축 하나에서 누적값 리셋, 데이터 누락, 역전(최종값 < 최초값) 이 발생하면, 그 축의 오류가 다른 정상 축의 값까지 깎아먹는다 — 전체를 먼저 더해버렸기 때문에 어느 축이 문제인지 구분할 수 없고 합계 전체가 왜곡된다.
해결: 축(카테고리)별로 먼저 “최종-최초” 차분을 계산 →그 다음에 축들을 합산하는 순서로 바꾼다.
-- ① 축별 최초/최종 스냅샷 추출 (ROW_NUMBER로 순서 부여)
WITH BASE AS (
SELECT category_key, seq_value,
ROW_NUMBER() OVER (PARTITION BY category_key ORDER BY event_time ASC) AS rn_first,
ROW_NUMBER() OVER (PARTITION BY category_key ORDER BY event_time DESC) AS rn_last
FROM raw_cumulative_table
WHERE event_time BETWEEN #{fromTime} AND #{toTime}
),
-- ② 축별로 먼저 차분 계산 + 음수 방어(리셋/역전 시 0으로 바닥)
DIFF AS (
SELECT a.category_key,
GREATEST(b.seq_value - a.seq_value, 0) AS delta
FROM BASE a
JOIN BASE b ON a.category_key = b.category_key
WHERE a.rn_first = 1 AND b.rn_last = 1
)
-- ③ 이제 축들을 합산해도 한 축의 이상치가 다른 축을 오염시키지 않음
SELECT SUM(delta) AS total_delta FROM DIFF;GREATEST(diff, 0)로 리셋·역전 케이스를 0으로 바닥 처리해 음수가 합계에 섞이는 것도 함께 방어한다.
함정 3 — MAX(CASE WHEN 축=N THEN 값 END)는 피벗용이지 합계용이 아니다
축별로 값을 컬럼으로 펼치는(피벗) 관용구로 MAX(CASE WHEN ...)을 쓰는 경우가 있는데, 이건 “그 축에 값이 하나뿐”이라는 전제가 있을 때만 안전하다. 그 축 안에 여러 하위 항목(세부 카테고리 등)이 섞여 있으면 가장 큰 값 하나만 살아남고 나머지는 버려진다.
-- 위험 — 축 안에 하위 항목이 여러 개면 그 중 최댓값 하나만 남음
MAX(CASE WHEN axis = 1 THEN value END) AS axis1_value해결: 피벗이 아니라 합계가 필요하면 SUM(CASE WHEN ...)으로 바꾼다.
-- 올바름 — 축 안의 모든 하위 항목 값을 합산
SUM(CASE WHEN axis = 1 THEN value ELSE 0 END) AS axis1_total요약 원칙
- 누적값 집계는 가능하면 세부 축이 살아있는 원천 테이블을 기준으로 한다(이미 합산된 요약 테이블에 의존하지 않는다).
- 여러 축의 누적값을 뺄셈으로 증분화할 때는 축별로 먼저 차분 → 그 다음 합산 순서를 지킨다(전체를 먼저 더한 뒤 한 번에 빼지 않는다). 리셋/역전 가능성이 있으면
GREATEST(diff, 0)로 방어한다. MAX(CASE WHEN...)은 “그 축에 값이 유일할 때”만 안전한 피벗 관용구다 — 합계가 필요하면SUM(CASE WHEN...)을 쓴다.